package com.ylr.base.framework.aspect;

import com.ylr.base.common.annotation.Resubmit;
import com.ylr.base.common.constant.Constant;
import com.ylr.base.common.enums.ResultEnum;
import com.ylr.base.common.exception.YlrException;
import com.ylr.base.common.po.user.UserPo;
import com.ylr.base.common.tool.Result;
import com.ylr.base.common.utils.DataUtils;
import com.ylr.base.common.utils.UserUtils;
import com.ylr.redis.framework.service.RedisService;
import com.ylr.base.common.utils.HttpUtils;
import com.ylr.redis.framework.service.RedisStringService;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.*;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.annotation.Order;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.ParseException;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.stereotype.Component;
import org.springframework.validation.BindingResult;
import org.springframework.web.multipart.MultipartFile;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Objects;
import java.util.concurrent.TimeUnit;

/**
 * Created by IntelliJ IDEA.
 *
 * @author wyw
 * date: 2023-04-21 18:33:52
 * className: ResubmitAspect 防重复提交切面类
 * version: 1.0
 * description:
 *  `@AllArgsConstructor`: 构造函数注入bean
 */
@Aspect
@Order(1)
@Component
@AllArgsConstructor
public class ResubmitAspect {

    private static final Logger log = LoggerFactory.getLogger(ResubmitAspect.class);

    /**
     * 放重复键数据存储对象
     * 每个项目单独持有一份
     */
    private static final ThreadLocal<ResubmitBo> threadLocal = new ThreadLocal<>();

    private final RedisStringService redisStringService;

    private final RedisService redisService;

    /**
     * 前置通知
     * `@Before`：本方法会在目标方法执行之前执行
     */
    @Before("@annotation(resubmit)")
    public void checkResubmit(JoinPoint point, Resubmit resubmit) {

        MethodSignature methodSignature = (MethodSignature) point.getSignature();
        // 获取目标类名称
        String className = methodSignature.getDeclaringType().getSimpleName();
        // 方法参数数组
        Object[] args = point.getArgs();

        HttpServletRequest request = HttpUtils.getHttpServletRequest();
        String fingerprint = request.getHeader(Constant.FINGERPRINT);
        // 请求ip地址
        String requestIp = (String) request.getAttribute(Constant.REQUEST_IP);

        StringBuilder sb = new StringBuilder();

        if (StringUtils.isBlank(resubmit.spEL())) {
            // 构造接口防重复请求Key
            sb.append(fingerprint).append(Constant.SPLIT).append(requestIp).append(Constant.SPLIT).append(className)
                    .append(Constant.SPLIT).append(methodSignature.getName()).append(Constant.SPLIT).append('[');
            // 入参过滤及格式化
            this.filterArgs(sb, args);
            // 替换倒数第一个字符
            sb.setCharAt(sb.length() - 1, ']');
        } else {
            /*
                SpringEL语句不为空，通过spEL语句生成缓存键
                参与spEL语句的参数值不能为空，则否报空指针异常
             */
            // 参数名数组
            String[] argNames = methodSignature.getParameterNames();
            // SpringEL语句解析器
            ExpressionParser parser = new SpelExpressionParser();
            // SpringEL上下文
            EvaluationContext ctx = new StandardEvaluationContext();
            for (int index = 0; index < args.length; index++) {
                if (this.filterArgs(args[index])) {
                    // 可以进行校验，将参数设置到上下文中
                    ctx.setVariable(argNames[index], args[index]);
                }
            }
            UserPo userPo = UserUtils.getUser();
            if (Objects.nonNull(userPo)) {
                // 设置请求用户
                ctx.setVariable("userPo", userPo);
            }

            // 生成key
            String key;
            try {
                key = parser.parseExpression(resubmit.spEL()).getValue(ctx, String.class);
            } catch (ParseException e) {
                log.error("SpringEl语句解析异常，加锁执行失败。message={}", e.getMessage(), e);
                throw new YlrException(ResultEnum.SERVICE_FAIL.getMsg());
            }
            sb.append(key);
        }
        String resubmitKey = sb.toString();

        // 获取限制时长，单位：毫秒
        long millisecond = resubmit.resubmitUnit().toMillis(resubmit.resubmitTime());
        // 最低限制时长1000毫秒
        millisecond = Math.max(1000, millisecond);

        // 操作Redis缓存，添加一对Key-Value值
        Boolean bool = redisStringService.setIfAbsent(resubmitKey, Constant.SPLIT, millisecond, TimeUnit.MILLISECONDS);
        if (!bool) {
            // 重复提交，直接返回失败结果
            String message = DataUtils.replaceStringBuilder(sb, resubmit.message(), "请求重复，系统拒绝此次请求。").toString();
            // 重复提交，直接抛出异常，不往下执行实际业务
            throw new YlrException(ResultEnum.REQUEST_FAIL.getCode(), message);
        }
        // 缓存放重复提交数据
        threadLocal.set(new ResubmitBo(resubmitKey, millisecond));
    }

    /**
     * 返回通知
     * `@AfterReturning`：本方法会在目标方法返回后调用
     */
    @AfterReturning(value = "@annotation(com.ylr.base.common.annotation.Resubmit)", returning = "result")
    public void afterReturning(Object result) {
//        log.info("返回通知");
        if (result instanceof Result) {
            Integer code = ((Result<?>) result).getCode();
            if (!ResultEnum.SUCCESS.getCode().equals(code)) {
                // 获取缓存键
                ResubmitBo resubmitBo = threadLocal.get();
                /*
                    System.currentTimeMillis() - resubmitBo.getNow() < resubmitBo.getMillisecond()
                    只有处于限制范围内，才能将防重复提交key删除
                 */
                if (Objects.nonNull(resubmitBo) && System.currentTimeMillis() - resubmitBo.getNow() < resubmitBo.getMillisecond()) {
                    // 清理缓存键
                    redisService.delete(resubmitBo.getResubmitKey());
                }
            }
        }
    }

    /**
     * 异常通知
     * `@AfterThrowing`：本方法会在目标方法抛出异常后调用
     */
    @AfterThrowing("@annotation(com.ylr.base.common.annotation.Resubmit)")
    public void afterThrowing() {
//        log.info("异常通知");
        // 获取缓存键
        ResubmitBo resubmitBo = threadLocal.get();
        if (Objects.nonNull(resubmitBo) && System.currentTimeMillis() - resubmitBo.getNow() < resubmitBo.getMillisecond()) {
            // 清理缓存键
            redisService.delete(resubmitBo.getResubmitKey());
        }
    }

    /**
     * 后置通知
     * `@After`：本方法会在目标方法返回或抛出异常后调用
     */
    @After("@annotation(com.ylr.base.common.annotation.Resubmit)")
    public void after() {
        // 清理缓存键
        threadLocal.remove();
    }

    /**
     * 参数过滤
     * 去除一些框架传入的参数对象
     *
     * @param sb   中间结果
     * @param args 参数数组
     */
    private void filterArgs(StringBuilder sb, Object[] args) {
        for (Object arg : args) {
            if (arg instanceof MultipartFile) {
                continue;
            }
            if (arg instanceof HttpServletRequest) {
                continue;
            }
            if (arg instanceof HttpServletResponse) {
                continue;
            }
            if (arg instanceof BindingResult) {
                continue;
            }
            sb.append(arg).append('、');
        }
    }

    /**
     * 参数过滤
     * 去除一些框架传入的参数对象
     * @param arg 参数对象
     */
    private boolean filterArgs(Object arg) {
        if (arg instanceof MultipartFile) {
            return false;
        }
        if (arg instanceof HttpServletRequest) {
            return false;
        }
        if (arg instanceof HttpServletResponse) {
            return false;
        }
        if (arg instanceof BindingResult) {
            return false;
        }
        return true;
    }

    /**
     * 防重复提交内部数据类
     */
    @Getter
    @Setter
    @ToString
    private static class ResubmitBo {

        /**
         * 放重复提交key
         */
        private String resubmitKey;

        /**
         * 请求时间戳
         */
        private long now;

        /**
         * 限制时长，单位：毫秒
         */
        private long millisecond;

        public ResubmitBo(String resubmitKey, long millisecond) {
            this.resubmitKey = resubmitKey;
            this.now = System.currentTimeMillis();
            this.millisecond = millisecond;
        }
    }
}
